localStorageを使ったVueプロジェクトのユニットテストがnot definedでコケる件
こんにちは。サービスグループの武田です。
ユニットテスト、書いてますか?ある程度大きくなってしまったプロダクトにゼロからテストを書くのはたいへんですよね。というわけで、小さいうちからテストは書くべきです。今回スモールスタートでlocalStorageを使ったプログラムを書いて、それのユニットテストを書いたところコケてしまいました。調べてみるとVueに限らず、フロントのユニットテストでは あるある なようですので、忘れないためにもエントリにしておきます。
環境
今回の検証環境は次のような環境になっています。
$ node -v v10.15.3 $ vue -V 3.8.2 $ sw_vers ProductName: Mac OS X ProductVersion: 10.14.5 BuildVersion: 18F132
そのほか、主なモジュールのバージョンは以下となっています。また、vueコマンドがインストールされていない場合は、npm install -g @vue/cli
でインストールします。
プロジェクトを作成
まずはVue CLIを利用してプロジェクトを作成します。
$ vue create example-vue-using-localstorage-unittest
presetはManualにして、すべてデフォルトを選択します。ここでテストスイートとしてmochaとchaiが選択されます。
最後の項目を選択するとインストールが始まります。インストールが終われば準備は完了です。
$ cd example-vue-using-localstorage-unittest
カウントアップの実装
今回は、アクセスするたびに回数をカウントアップしていくだけの機能を実装してみます。アクセス数の保存先としてlocalStorageを使用します。
またソースコード全体はGitHubに上げてあります。
テストファーストであれば先にテストを書くわけですが、細かいことは置いておいてまずは動くものを実装しましょう。省エネで進めるため、すでに作成されているAbout.vue
を修正して実装します。
<template> <div class="about"> <h1>This is an about page</h1> <p>{{ count }}</p> </div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; @Component export default class About extends Vue { get count() { return Number(localStorage.getItem('count')); } private created() { const c = localStorage.getItem('count'); const count = c === null ? 0 : Number(c); localStorage.setItem('count', String(count + 1)); } } </script>
特に特筆すべき箇所はありませんが、localStorageのアクセスはgetItem/setItem
の利用が推奨されていることを最近知りました。これでアプリケーションを実行してみれば動作が確認できます。Aboutのページにアクセスするたび、数字がカウントアップされていきます。
$ npm run serve -- --open
テストケースの作成と実行
先ほど作成した機能のテストケースを作成します。初回アクセスと複数回アクセスした場合がテストできていればよいのでこんな感じでしょうか?
/* tslint:disable:no-unused-expression */ import { expect } from 'chai'; import { shallowMount } from '@vue/test-utils'; import About from '@/views/About.vue'; describe('About.vue', () => { it('render 1 with first access', () => { const wrapper = shallowMount(About); const p = wrapper.find('p'); expect(p.is('p')).to.be.true; expect(p.text()).to.equal('1'); }); it('render same count with some access', () => { let wrapper = shallowMount(About); wrapper = shallowMount(About); wrapper = shallowMount(About); const p = wrapper.find('p'); expect(p.is('p')).to.be.true; expect(p.text()).to.equal('3'); }); });
それではユニットテストを走らせてみます。次のコマンドで実行できます。
$ npm run test:unit
結果は次のようになりました。……どうやら、localStorageが未定義で失敗しています。
WEBPACK Compiled successfully in 4485ms MOCHA Testing... About.vue [Vue warn]: Error in created hook: "ReferenceError: localStorage is not defined" found in ---> <About> at src/views/About.vue
localStorageを自前定義して回避
ユニットテストの環境にlocalStorageがないわけですが、自分で定義して回避するのが一番手軽な方法のようです。次のようなブートストラップコードを用意し、テストランナーが読み込むように設定をします。
const localStorageMock = (() => { let store = {}; return { getItem(key) { return store[key] || null; }, setItem(key, value) { store[key] = value.toString(); }, clear() { store = {}; }, }; })(); // global define localStorage = localStorageMock;
なお、最後のグローバルオブジェクトとして設定している箇所ですが、defineProperty
を使った方法はうまく動きませんでした。
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
そして、package.json
を修正して、自動的にロードされるようにします。
"scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint", "test:e2e": "vue-cli-service test:e2e", "test:unit": "vue-cli-service test:unit -r tests/unit/bootstrap.js" },
それでは再度テストを走らせます。
WEBPACK Compiled successfully in 4232ms MOCHA Testing... About.vue ✓ render 1 with first access 1) render same count with some access HelloWorld.vue ✓ renders props.msg when passed 2 passing (193ms) 1 failing 1) About.vue render same count with some access: AssertionError: expected '4' to equal '3' + expected - actual -4 +3 at Context.it (dist/webpack:/tests/unit/About.spec.ts:23:1)
localStorage is not defined のエラーは解消されてますね!ただ肝心のテストに失敗しています。これはlocalStorageの状態が残っていることが原因ですね。毎回localStorageをクリアするようにテストを修正してみます。
describe('About.vue', () => { beforeEach(() => { localStorage.clear(); }); it('render 1 with first access', () => {
修正できたらテストを再実行します。
WEBPACK Compiled successfully in 4279ms MOCHA Testing... About.vue ✓ render 1 with first access ✓ render same count with some access HelloWorld.vue ✓ renders props.msg when passed 3 passing (92ms) MOCHA Tests completed successfully
うまく動きました!
まとめ
これからユニットテスト書くぞ!という方の助けになれば幸いです。